package com.ztm.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ztm.shortlink.project.common.convention.exception.ClientException;
import com.ztm.shortlink.project.common.convention.exception.ServiceException;
import com.ztm.shortlink.project.common.enums.ValidDateTypeEnum;
import com.ztm.shortlink.project.dao.entity.ShortLinkDO;
import com.ztm.shortlink.project.dao.entity.ShortLinkGotoDO;
import com.ztm.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.ztm.shortlink.project.dao.mapper.ShortLinkMapper;
import com.ztm.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.ztm.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.ztm.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.ztm.shortlink.project.dto.resp.ShortLinkCountQueryRespDTO;
import com.ztm.shortlink.project.dto.resp.ShortLinkCreateRespDTO;
import com.ztm.shortlink.project.dto.resp.ShortLinkPageRespDTO;
import com.ztm.shortlink.project.service.ShortLinkService;
import com.ztm.shortlink.project.utils.HashUtil;
import com.ztm.shortlink.project.utils.LinkUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static com.ztm.shortlink.project.common.constant.RedisKeyConstant.*;

/**
 * 短链接接口实现层
 */
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUrlCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;

    /**
     * 创建短链接
     * @param requestParam
     * @return
     */
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        //根据原始链接生成短链接Uri
        String shortUri = generateUri(requestParam);
        //拼接域名和Uri得到fullShortUrl
        String fullShortUrl = requestParam.getDomain() + "/" + shortUri;

        //构建短链接实体
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(requestParam.getDomain())
                .shortUri(shortUri)
                .fullShortUrl(fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .enableStatus(0)
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .build();
        //如果有效期类型为永久，则有效期设置为null
        if(Objects.equals(requestParam.getValidDateType(),ValidDateTypeEnum.PERMANENT.getType())){
            shortLinkDO.setValidDate(null);
        }
        //构建短链接路由表实体
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        try {
            //插入数据库
            shortLinkGotoMapper.insert(shortLinkGotoDO);
            baseMapper.insert(shortLinkDO);
        } catch (DuplicateKeyException ex) {
            //高并发情况下，可能多个线程同时拿到相同的fullShortUrl,并且判断都不在布隆过滤器
            throw new ServiceException("短链接生成重复");
        }

        //创建短链接后，缓存预热，把短链接设置到缓存中去
        stringRedisTemplate.opsForValue().set(
                GOTO_ORIGIN_URL_KEY + fullShortUrl, requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MILLISECONDS);

        //短链接加入到布隆过滤器
        shortUrlCachePenetrationBloomFilter.add(fullShortUrl);

        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl(fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    /**
     * 根据gid分页查询短链接
     * @param requestParam
     * @return
     */
    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getEnableStatus, 0)
                .eq(ShortLinkDO::getDelFlag, 0)
                .orderByDesc(ShortLinkDO::getCreateTime);
        IPage<ShortLinkDO> resultPage = baseMapper.selectPage(requestParam, queryWrapper);
        return resultPage.convert(each -> BeanUtil.toBean(each, ShortLinkPageRespDTO.class));
    }

    /**
     * 短链接分组内数量
     * @param requestParam
     * @return
     */
    @Override
    public List<ShortLinkCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        //根据gid列表查询所有短链接，并按照gid分组
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid,count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag",0)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList,ShortLinkCountQueryRespDTO.class);
    }

    /**
     * 修改短链接
     * @param requestParam
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        //先查询到要修改的短链接
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if(hasShortLinkDO==null){
            throw new ClientException("短链接不存在");
        }

        //要修改后的短链接实体
        ShortLinkDO shortLinkDO;

        if(Objects.equals(hasShortLinkDO.getGid(),requestParam.getGid())){
            //如果gid不修改，则在原来的记录上更新

            //构建更新短链接实体
            //原始链接、有效期类型、有效期、描述
            shortLinkDO = ShortLinkDO.builder()
                    .originUrl(requestParam.getOriginUrl())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .build();

            //更新条件
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()),
                            ShortLinkDO::getValidDate, null);

            //更新
            baseMapper.update(shortLinkDO,updateWrapper);
        }else{
            //如果修改了gid，那么由于分表，所以先删除原来的短链接，再插入新的短链接

            //构建更新短链接实体
            //原始链接、gid、有效期类型、有效期、描述
            shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                    .originUrl(requestParam.getOriginUrl())
                    .clickNum(hasShortLinkDO.getClickNum())
                    .gid(requestParam.getGid())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .build();
            //如果有效期类型为永久，则有效期设置为null
            if(Objects.equals(requestParam.getValidDateType(),ValidDateTypeEnum.PERMANENT.getType())){
                shortLinkDO.setValidDate(null);
            }
            //删除原记录
            baseMapper.delete(queryWrapper);

            //插入新记录
            baseMapper.insert(shortLinkDO);

        }

    }

    /**
     * 短链接跳转
     * @param shorUri
     * @param request
     * @param response
     */
    @Override
    public void restoreUrl(String shorUri, HttpServletRequest request, HttpServletResponse response){
        //获取域名
        String serverName = request.getServerName();
        //拼接 域名+shortUri 得到完整短链接
        String fullShortUrl = serverName + "/" + shorUri;
        //根据短链接从缓存中查询原始链接
        String originUrl = stringRedisTemplate.opsForValue().get(GOTO_ORIGIN_URL_KEY + fullShortUrl);
        //如果缓存中查到原始链接，直接重定向至原始链接
        if(StrUtil.isNotBlank(originUrl)){
            try {
                response.sendRedirect(originUrl);
                return;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //缓存中没有查到原始链接

        //布隆过滤器判断短链接是否存在(防止缓存穿透)
        if(!shortUrlCachePenetrationBloomFilter.contains(fullShortUrl)){
            try {
                response.sendRedirect("/page/notfound");
                return;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //布隆过滤器判断存在，但是通过缓存空值发现不存在
        String gotoIsNull = stringRedisTemplate.opsForValue().get(GOTO_IS_NULL_KEY + fullShortUrl);
        if(StrUtil.isNotBlank(gotoIsNull)){
            //跳转至短链接不存在页面
            try {
                response.sendRedirect("/page/notfound");
                return;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //使用分布式锁防止缓存击穿
        RLock lock = redissonClient.getLock(LOCK_GOTO_ORIGIN_URL_KEY + fullShortUrl);
        //加锁
        lock.lock();
        try {

            //双重检测，获取到锁再次查询缓存
            //根据短链接从缓存中查询原始链接
            originUrl = stringRedisTemplate.opsForValue().get(GOTO_ORIGIN_URL_KEY + fullShortUrl);
            //如果缓存中查到原始链接，直接跳转
            if(StrUtil.isNotBlank(originUrl)){
                try {
                    response.sendRedirect(originUrl);
                    return;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            //查询数据库
            //根据短链接从t_link_goto表查询分组标识gid，因为gid是分片键
            LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);

            if(shortLinkGotoDO==null){
                //布隆过滤器有可能误判，判断为存在实际上不存在，存空值
                stringRedisTemplate.opsForValue().set(GOTO_IS_NULL_KEY + fullShortUrl,"",
                        30, TimeUnit.MINUTES);
                try {
                    response.sendRedirect("/page/notfound");
                    return;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            //根据短链接查询原始链接，gid是分片键
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            if(shortLinkDO==null||
                    (shortLinkDO.getValidDate()!=null&&shortLinkDO.getValidDate().before(new Date()))){
                //存空值
                stringRedisTemplate.opsForValue().set(GOTO_IS_NULL_KEY + fullShortUrl,"",
                        30, TimeUnit.MINUTES);

                //跳转至短链接不存在页面
                try {
                    response.sendRedirect("/page/notfound");
                    return;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            //把查询的数据设置到缓存中去
            stringRedisTemplate.opsForValue().set(
                    GOTO_ORIGIN_URL_KEY + shortLinkDO.getFullShortUrl(), shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),TimeUnit.MILLISECONDS);

            try {
                //重定向
                response.sendRedirect(shortLinkDO.getOriginUrl());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }finally {
            //解锁
            lock.unlock();
        }


    }

    //根据原始链接生成短链接Uri
    private String generateUri(ShortLinkCreateReqDTO requestParam){
        String shortUri;
        int customGenerateCount = 0;
        while(true){
            //如果原始链接连续生成10次Uri还发生冲突
            if(customGenerateCount>10){
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            //取出原始链接
            String originUrl = requestParam.getOriginUrl();

            //原始链接再加上一个随机数，可以减小发生冲突的概率

            //如果使用时间戳，
            //对于一个线程内，可能在同一时间连续生成10次一样的Uri
            //对于多线程，同一时刻用相同的原始链接会生成相同的Uri,
            // 并且判断都不在布隆过滤器可能导致数据库插入时主键冲突
            originUrl += UUID.randomUUID().toString();


            //生成Uri
            shortUri = HashUtil.hashToBase62(originUrl);

            //如果Uri不存在，则退出while循环
            if(!shortUrlCachePenetrationBloomFilter.contains(requestParam.getDomain()+"/"+shortUri)){
                break;
            }

            //循环次数+1
            customGenerateCount++;
        }
        return shortUri;
    }
}
